/*
 * ﻿Copyright (C) 2011-2013 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.aspectjutil.eventmanager;

import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.WeakHashMap;

import org.aspectj.lang.Aspects;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.ConstructorSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.newmainsoftech.aspectjutil.eventmanager.label.EventListener;

/** 
 * {@link Aspect} to truck and preserve constructed object of event listener class annotated 
 * with &#64;{@link EventListener}. 
 * 
 * @author <a href="mailto:artymt@gmail.com">Arata Y.</a>
 */
@Aspect( "pertypewithin( @com.newmainsoftech.aspectjutil.eventmanager.label.EventListener *)")
public class EventListenerInstanceTracker {
//TODO It may be worthy to research whether it is possible to implement this by extending AbstractProcessor.
	protected Logger logger = LoggerFactory.getLogger( this.getClass());
	
	public static class EventListenerTimeStamp {
		private final long threadId;
			public long getThreadId() {
				return threadId;
			}
		private final long time;
			public long getTime() {
				return time;
			}
		public EventListenerTimeStamp( long threadId, long time) {
			this.threadId = threadId;
			this.time = time;
		}
	}
	
	private final Object mapsLock = new Object(); 
		Object getMapsLock() {
			return mapsLock;
		}
	private Map<Object, EventListenerTimeStamp> listenerMap 
	= Collections.synchronizedMap( new LinkedHashMap<Object, EventListenerTimeStamp>());
		Map<Object, EventListenerTimeStamp> getListenerMap() {
			return listenerMap;
		}
	private Map<Object, EventListenerTimeStamp> listenerWeakHashMap 
	= Collections.synchronizedMap( new WeakHashMap<Object, EventListenerTimeStamp>());
		Map<Object, EventListenerTimeStamp> getListenerWeakHashMap() {
			return listenerWeakHashMap;
		}
	
	/**
	 * Dummy method for {@link Pointcut} what cross-cut constructor of event listener class 
	 * annotated with {@link EventListener}. 
	 * This <code>Pointcut</code> is advised by {@link #afterEventListenerConstructor(Object)}. 
	 * 
	 * @param obj
	 */
	@Pointcut( "initialization( new(..)) && this( obj)")
	public static void eventListenerConstructorPointcut( Object obj) {}
	
	/**
	 * {@link AfterReturning} advise method to preserve instance of event listener class annotated 
	 * with {@link EventListener} at its construction.
	 * There shouldn't be necessity of calling this method intentionally. 
	 * 
	 * @param obj
	 * @see #eventListenerConstructorPointcut(Object)
	 */
	@AfterReturning( pointcut="eventListenerConstructorPointcut( obj)", argNames="obj")
	public void afterEventListenerConstructor( JoinPoint joinPoint, Object obj) {
		ConstructorSignature constructorSignature = (ConstructorSignature)(joinPoint.getSignature());
		Class<?> constructorClass = constructorSignature.getDeclaringType();
			// screen sub classes out
			if ( !constructorClass.equals( obj.getClass())) return;
		
		EventListenerTimeStamp eventListenerTimeStamp
		= new EventListenerTimeStamp( Thread.currentThread().getId(), System.currentTimeMillis());
		synchronized( getMapsLock()) {
			if ( constructorClass.getAnnotation( EventListener.class).gcSubject()) {
				getListenerWeakHashMap().put( obj, eventListenerTimeStamp);
			}
			else {
				getListenerMap().put( obj, eventListenerTimeStamp);
			}
		} // synchronized( mapsLock)
		
			if ( logger.isDebugEnabled()) {
				logger.debug( 
						String.format(
								"Event lister instance %1$s has been registered%2$s. ",
								obj.toString(), 
								(constructorClass.getAnnotation( EventListener.class).gcSubject() 
										? " (as subject to GC)" : "")
								)
						);
			}
	}
	
	protected void dumpListenerInstanceMap( LinkedHashMap<Object, Long> listenerInstanceMap) {
		if ( logger.isDebugEnabled()) {
			logger.debug( 
					String.format(
							"Listers of event: %n%1$s",
							listenerInstanceMap.toString()
							)
					);
		}
	}
	
	static class EventListenerTimeStampComparator implements Comparator<Object> {
		protected Logger logger = LoggerFactory.getLogger( this.getClass());
		
		private Map<Object, EventListenerTimeStamp> listenerMap;
		private Map<Object, EventListenerTimeStamp> listenerWeakHashMap;
		private Object mapsLock;
		
		public EventListenerTimeStampComparator(
				Map<Object, EventListenerTimeStamp> listenerMap,
				Map<Object, EventListenerTimeStamp> listenerWeakHashMap, 
				Object mapsLock) {
			this.listenerMap = listenerMap;
			this.listenerWeakHashMap = listenerWeakHashMap;
			this.mapsLock = mapsLock;
		}

		@Override
		public int compare( Object arg0, Object arg1) {
			if ( arg0 == arg1) return 0;
			if ( arg0 == null) return -1;
			if ( arg1 == null) return 1;
				
			EventListenerTimeStamp eventListenerTimeStamp0 = null;
			EventListenerTimeStamp eventListenerTimeStamp1 = null;
			synchronized( mapsLock) {
				if ( listenerMap.containsKey( arg0)) {
					eventListenerTimeStamp0 = listenerMap.get( arg0);
				}
				else if ( listenerWeakHashMap.containsKey( arg0)) {
					eventListenerTimeStamp0 = listenerWeakHashMap.get( arg0);
				}
				else {
					if ( logger.isWarnEnabled()) {
						logger.warn( 
								String.format(
										"%1$s is not found in registered listener map.",
										(( arg0 == null) ? "null" : arg0.toString())
										));
					}
				}
				if ( listenerMap.containsKey( arg1)) {
					eventListenerTimeStamp1 = listenerMap.get( arg1);
				}
				else if ( listenerWeakHashMap.containsKey( arg1)) {
					eventListenerTimeStamp1 = listenerWeakHashMap.get( arg1);
				}
				else {
					if ( logger.isWarnEnabled()) {
						logger.warn( 
								String.format(
										"%1$s is not found in registered listener map.",
										(( arg1 == null) ? "null" : arg1.toString())
										));
					}
				}
			} // synchronized( mapsLock)
			
			if ( eventListenerTimeStamp0 == eventListenerTimeStamp1) {
				return 0;
			}
			if ( eventListenerTimeStamp0 == null) return -1;
			if ( eventListenerTimeStamp1 == null) return 1;
			
			if ( eventListenerTimeStamp0.getTime() < eventListenerTimeStamp1.getTime()) return -1;
			if ( eventListenerTimeStamp0.getTime() > eventListenerTimeStamp1.getTime()) return 1;
			
			if ( eventListenerTimeStamp0.getThreadId() < eventListenerTimeStamp1.getThreadId()) return -1;
			if ( eventListenerTimeStamp0.getThreadId() > eventListenerTimeStamp1.getThreadId()) return 1;
			
			if ( arg0.hashCode() < arg1.hashCode()) return -1;
			if ( arg0.hashCode() > arg1.hashCode()) return 1;
			return 0;
		}
	}
	EventListenerTimeStampComparator eventListenerTimeStampComparator
	= new EventListenerTimeStampComparator( getListenerMap(), getListenerWeakHashMap(), getMapsLock());
		EventListenerTimeStampComparator getEventListenerTimeStampComparator() {
			return eventListenerTimeStampComparator;
		}

	protected TreeMap<Object, EventListenerTimeStamp> getSortedListeners() {
		TreeMap<Object, EventListenerTimeStamp> mapSortedByValue 
		= new TreeMap<Object, EventListenerTimeStamp>( getEventListenerTimeStampComparator());
		
		synchronized( getMapsLock()) {
			mapSortedByValue.putAll( getListenerMap());
			mapSortedByValue.putAll( getListenerWeakHashMap());
		} // synchronized( mapsLock)
		
		return mapSortedByValue;
	}
	
	/**
	 * Obtain existing instances of event listener class specified. 
	 * Implemented to be called via {@link Aspects#aspectOf(Class, Class)}. 
	 * 
	 * @return {@link Map} consists of {@link Entry} of existing event listener object and 
	 * ID of thread what event listener object has been constructed by.
	 */
	public Map<Object, Long> getListeners() {
		LinkedHashMap<Object, Long> instanceMap = new LinkedHashMap<Object, Long>();
		
		TreeMap<Object, EventListenerTimeStamp> sortedListenerMap = getSortedListeners();
		for( Entry<Object, EventListenerTimeStamp> entry : sortedListenerMap.entrySet()) {
			instanceMap.put( entry.getKey(), entry.getValue().getThreadId());
		} // for
			dumpListenerInstanceMap( instanceMap);
		
		return instanceMap;
	}
	
	/**
	 * Obtain existing instances of event listener class specified. 
	 * Implemented to be called via {@link Aspects#aspectOf(Class, Class)}. 
	 * 
	 * @param threadId specifies ID of thread what constructed instance of event listener class 
	 * annotated by {@link EventListener}
	 * @return {@link Map} consists of {@link Entry} of existing event listener object and 
	 * ID of thread what event listener object has been constructed by.
	 */
	public Map<Object, Long> getListeners( long threadId) {
		LinkedHashMap<Object, Long> instanceMap = new LinkedHashMap<Object, Long>();
		
		TreeMap<Object, EventListenerTimeStamp> sortedListenerMap = getSortedListeners();
		for( Entry<Object, EventListenerTimeStamp> entry : sortedListenerMap.entrySet()) {
			Long threadIdLong = entry.getValue().getThreadId();
			if ( threadId != threadIdLong.longValue()) continue; // for
			instanceMap.put( entry.getKey(), threadIdLong);
		} // for
			dumpListenerInstanceMap( instanceMap);
		
		return instanceMap;
	}
	
	/**
	 * Obtain existing instances of event listener class specified. 
	 * Implemented to be called via {@link Aspects#aspectOf(Class, Class)}. 
	 * 
	 * @param listenerClass event listener class annotated with {@link EventListener}
	 * @return {@link Map} consists of {@link Entry} of existing event listener object and 
	 * ID of thread what event listener object has been constructed by.
	 */
	public Map<Object, Long> getListeners( Class<?> listenerClass) {
		LinkedHashMap<Object, Long> instanceMap = new LinkedHashMap<Object, Long>();
		
		TreeMap<Object, EventListenerTimeStamp> sortedListenerMap = getSortedListeners();
		for( Entry<Object, EventListenerTimeStamp> entry : sortedListenerMap.entrySet()) {
			if ( !entry.getKey().getClass().equals( listenerClass)) continue; //for
			Long threadIdLong = entry.getValue().getThreadId();
			instanceMap.put( entry.getKey(), threadIdLong);
		} // for
			dumpListenerInstanceMap( instanceMap);
			
		return instanceMap;
	}
	
	/**
	 * Obtain existing instances of event listener class specified. 
	 * Implemented to be called via {@link Aspects#aspectOf(Class, Class)}. 
	 * 
	 * @param listenerClass event listener class annotated with {@link EventListener}
	 * @param threadId specifies ID of thread what constructed instance of class specified by 
	 * <code>listenerClass</code> input.
	 * @return {@link Map} consists of {@link Entry} of existing event listener object and 
	 * ID of thread what event listener object has been constructed by.
	 */
	public Map<Object, Long> getListeners( Class<?> listenerClass, long threadId) {
		LinkedHashMap<Object, Long> instanceMap = new LinkedHashMap<Object, Long>();
		
		TreeMap<Object, EventListenerTimeStamp> sortedListenerMap = getSortedListeners();
		for( Entry<Object, EventListenerTimeStamp> entry : sortedListenerMap.entrySet()) {
			if ( !entry.getKey().getClass().equals( listenerClass)) continue; //for
			Long threadIdLong = entry.getValue().getThreadId();
			if ( threadId != threadIdLong.longValue()) continue; // for
			instanceMap.put( entry.getKey(), threadIdLong);
		} // for
			dumpListenerInstanceMap( instanceMap);
			
		return instanceMap;
	}
	
	/**
	 * Clear all {@link Entry} of tracked instance of event listener class annotated 
	 * with {@link EventListener} 
	 */
	public void clear() {
		synchronized( getMapsLock()) {
			getListenerMap().clear();
			getListenerWeakHashMap().clear();
		} // synchronized
	}
}
